Getting Started with Platform Development
This guide will help you get started building platforms in the metastate ecosystem. We'll cover the essential concepts and patterns you'll need to implement, using @eCurrency-api as a reference example.
Overview
Platforms in the metastate ecosystem follow a standard architecture pattern:
- Authentication — Users authenticate using their W3ID (Web3 Identity) via the
w3ds://authprotocol - Webhooks — Platform data syncs from the global eVault system via webhooks
- Mappings — Data transformation between global ontology and local database schemas
This document focuses on authentication. For webhooks and mappings, see the other documentation files.
Authentication
All platforms use a signature-based authentication system that leverages users' existing ename and keys attached to that. The authentication flow follows the w3ds://auth protocol.
Authentication Flow
The authentication process involves these steps:
- Client requests auth offer → Server returns
w3ds://authURL with session ID - User signs in via w3ds client → User is redirected back with signature
- Server verifies signature → Uses
signature-validatorto verify the signature - Server finds/creates user → Looks up user by eName, generates JWT token
- Client uses Bearer token → Includes token in
Authorization: Bearer <token>header - Middleware validates token →
authMiddlewareextracts token and loads user intoreq.user - Protected routes → Use
authGuardto ensure user is authenticated
Implementation Example (eCurrency-api)
1. Offer Endpoint (GET /api/auth/offer)
This endpoint generates an authentication offer URL that the client can use to initiate the login flow.
getOffer = async (req: Request, res: Response) => {
const baseUrl = "http://localhost:9888";
const url = new URL("/api/auth", baseUrl).toString();
const sessionId = uuidv4();
const offer = `w3ds://auth?redirect=${url}&session=${sessionId}&platform='PLATFORM NAME HERE`;
res.json({ offer, sessionId });
};
Response:
{
"offer": "w3ds://auth?redirect=http://localhost:9888/api/auth&session=abc123...&platform=ecurrency",
"sessionId": "abc123..."
}
The client opens this URL in a w3ds-compatible client (like the eID Wallet), which handles the user's signature and redirects back to your platform.
2. Login Endpoint (POST /api/auth)
This endpoint receives the authentication result from the w3ds client and verifies the signature.
Request body:
{
"ename": "@user.w3id",
"session": "abc123...",
"w3id": "https://evault.example.com/users/123",
"signature": "z..."
}
Implementation:
login = async (req: Request, res: Response) => {
const { ename, session, signature } = req.body;
// Verify signature using signature-validator (see [Signing](/docs/W3DS%20Protocol/Signing) / [Signature Formats](/docs/W3DS%20Protocol/Signature-Formats))
const verificationResult = await verifySignature({
eName: ename,
signature: signature,
payload: session,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL,
});
if (!verificationResult.valid) {
return res.status(401).json({
error: "Invalid signature",
message: verificationResult.error
});
}
// Find user by eName (users must be created via webhook first)
const user = await this.userService.findUser(ename);
if (!user) {
return res.status(404).json({
error: "User not found",
message: "User must be created via [eVault](/docs/Infrastructure/eVault) [webhook](/docs/Post%20Platform%20Guide/webhook-controller) before authentication"
});
}
// Generate JWT token
const token = signToken({ userId: user.id });
res.status(200).json({
user: { /* user data */ },
token,
});
};
Key points:
- The
sessionstring is what was signed by the user - Signature verification uses the
signature-validatorpackage (see Signing), which:- Fetches the user's public key from their eVault
- Verifies the signature using Web Crypto API
- Supports multiple signature formats (multibase, base64, etc.)
- Users must exist in your database before they can authenticate (created via webhooks)
- The JWT token contains the
userIdand expires in 7 days
3. JWT Token Generation
The JWT token is generated using a secret key stored in JWT_SECRET environment variable.
// src/utils/jwt.ts
export const signToken = (payload: AuthTokenPayload): string => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" });
};
export const verifyToken = (token: string): AuthTokenPayload => {
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload & AuthTokenPayload;
if (!decoded.userId || typeof decoded.userId !== 'string') {
throw new Error("Invalid token: missing or invalid userId");
}
return { userId: decoded.userId };
};
Important: Always set JWT_SECRET as an environment variable and never commit it to version control.
4. Auth Middleware
The auth middleware extracts the JWT token from the Authorization header and loads the user into req.user.
// src/middleware/auth.ts
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next(); // Continue without user (for optional auth routes)
}
const token = authHeader.substring(7);
try {
const { userId } = verifyToken(token);
const user = await userService.getUserById(userId);
if (user) {
req.user = user;
}
} catch (error) {
// Invalid token - continue without user
}
next();
};
5. Auth Guard
The auth guard ensures that a user is authenticated before proceeding.
export const authGuard = (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
};
Route Configuration
Routes are configured to use middleware appropriately:
// Public routes (no auth required)
app.get("/api/auth/offer", authController.getOffer);
app.post("/api/auth", authController.login);
app.post("/api/webhook", webhookController.handleWebhook); // Webhooks don't require auth
// Protected routes (auth required)
app.use(authMiddleware); // Apply auth middleware to all routes below
app.get("/api/users/me", authGuard, userController.currentUser);
app.post("/api/currencies", authGuard, currencyController.createCurrency);
// ... other protected routes
Route patterns:
- Public routes: Authentication endpoints, webhooks, and any public-facing APIs
- Protected routes: All routes after
app.use(authMiddleware)require authentication - Optional auth routes: Routes that work with or without authentication (rare)
Environment Variables
Required environment variables for authentication:
# JWT secret for token signing/verification
JWT_SECRET=your-secret-key-here
# Registry base URL for signature verification
PUBLIC_REGISTRY_URL=https://registry.example.com
## References
- [Authentication](/docs/W3DS%20Protocol/Authentication) — w3ds://auth protocol
- [Signing](/docs/W3DS%20Protocol/Signing) — Signature creation and verification
- [Signature Formats](/docs/W3DS%20Protocol/Signature-Formats) — Cryptographic details
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — Receiving webhooks
- [Mapping Rules](/docs/Post%20Platform%20Guide/mapping-rules) — Schema mapping
- [eVault](/docs/Infrastructure/eVault) — Storage and key binding
- [Registry](/docs/Infrastructure/Registry) — W3ID resolution and JWKS